/*
 * Created on May 27, 2008
 * Created by Paul Gardner
 * 
 * Copyright (C) Azureus Software, Inc, All Rights Reserved.
 * 
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; version 2 of the License only.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
 */

package com.aelitis.azureus.plugins.net.buddy.tracker;

import java.net.InetAddress;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.gudy.azureus2.core3.global.GlobalManager;
import org.gudy.azureus2.core3.global.GlobalManagerAdapter;
import org.gudy.azureus2.core3.peer.PEPeerManager;
import org.gudy.azureus2.core3.util.AddressUtils;
import org.gudy.azureus2.core3.util.Average;
import org.gudy.azureus2.core3.util.Constants;
import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.core3.util.HashWrapper;
import org.gudy.azureus2.core3.util.LightHashMap;
import org.gudy.azureus2.core3.util.SHA1;
import org.gudy.azureus2.core3.util.SimpleTimer;
import org.gudy.azureus2.core3.util.SystemTime;
import org.gudy.azureus2.core3.util.TimerEvent;
import org.gudy.azureus2.core3.util.TimerEventPerformer;
import org.gudy.azureus2.core3.util.TimerEventPeriodic;
import org.gudy.azureus2.plugins.download.Download;
import org.gudy.azureus2.plugins.download.DownloadAnnounceResult;
import org.gudy.azureus2.plugins.download.DownloadListener;
import org.gudy.azureus2.plugins.download.DownloadManagerListener;
import org.gudy.azureus2.plugins.download.DownloadPeerListener;
import org.gudy.azureus2.plugins.download.DownloadScrapeResult;
import org.gudy.azureus2.plugins.download.DownloadTrackerListener;
import org.gudy.azureus2.plugins.peers.Peer;
import org.gudy.azureus2.plugins.peers.PeerEvent;
import org.gudy.azureus2.plugins.peers.PeerListener2;
import org.gudy.azureus2.plugins.peers.PeerManager;
import org.gudy.azureus2.plugins.peers.PeerManagerListener;
import org.gudy.azureus2.plugins.peers.PeerStats;
import org.gudy.azureus2.plugins.torrent.Torrent;
import org.gudy.azureus2.plugins.ui.config.BooleanParameter;
import org.gudy.azureus2.plugins.ui.config.Parameter;
import org.gudy.azureus2.plugins.ui.config.ParameterListener;
import org.gudy.azureus2.pluginsimpl.local.PluginCoreUtils;

import com.aelitis.azureus.core.AzureusCoreFactory;
import com.aelitis.azureus.core.util.CopyOnWriteList;
import com.aelitis.azureus.core.util.CopyOnWriteSet;
import com.aelitis.azureus.plugins.net.buddy.BuddyPlugin;
import com.aelitis.azureus.plugins.net.buddy.BuddyPluginAZ2TrackerListener;
import com.aelitis.azureus.plugins.net.buddy.BuddyPluginBuddy;
import com.aelitis.azureus.plugins.net.buddy.BuddyPluginListener;

public class BuddyPluginTracker implements BuddyPluginListener, DownloadManagerListener, BuddyPluginAZ2TrackerListener, DownloadPeerListener {
    private static final Object PEER_KEY = new Object(); // maps to Download object

    private static final Object PEER_STATS_KEY = new Object();

    public static final int BUDDY_NETWORK_IDLE = 1;
    public static final int BUDDY_NETWORK_OUTBOUND = 2;
    public static final int BUDDY_NETWORK_INBOUND = 3;

    private static final int TRACK_CHECK_PERIOD = 15 * 1000;
    private static final int TRACK_CHECK_TICKS = TRACK_CHECK_PERIOD / BuddyPlugin.TIMER_PERIOD;

    private static final int PEER_CHECK_PERIOD = 60 * 1000;
    private static final int PEER_CHECK_TICKS = PEER_CHECK_PERIOD / BuddyPlugin.TIMER_PERIOD;

    private static final int PEER_RECHECK_PERIOD = 120 * 1000;
    private static final int PEER_RECHECK_TICKS = PEER_RECHECK_PERIOD / BuddyPlugin.TIMER_PERIOD;

    private static final int TRACK_INTERVAL = 10 * 60 * 1000;

    private static final int SHORT_ID_SIZE = 4;
    private static final int FULL_ID_SIZE = 20;

    private static final int REQUEST_TRACKER_SUMMARY = 1;
    private static final int REPLY_TRACKER_SUMMARY = 2;
    private static final int REQUEST_TRACKER_STATUS = 3;
    private static final int REPLY_TRACKER_STATUS = 4;
    private static final int REQUEST_TRACKER_CHANGE = 5;
    private static final int REPLY_TRACKER_CHANGE = 6;
    private static final int REQUEST_TRACKER_ADD = 7;
    private static final int REPLY_TRACKER_ADD = 8;

    private static final int RETRY_SEND_MIN = 5 * 60 * 1000;
    private static final int RETRY_SEND_MAX = 60 * 60 * 1000;

    private static final int BUDDY_NO = 0;
    private static final int BUDDY_MAYBE = 1;
    private static final int BUDDY_YES = 2;

    private BuddyPlugin plugin;

    private boolean plugin_enabled;
    private boolean tracker_enabled;
    private boolean seeding_only;

    private boolean old_plugin_enabled;
    private boolean old_tracker_enabled;
    private boolean old_seeding_only;

    private int network_status = BUDDY_NETWORK_IDLE;

    private Set online_buddies = new HashSet();
    private Map online_buddy_ips = new HashMap();

    private Set tracked_downloads = new HashSet();
    private int download_set_id;

    private Set last_processed_download_set;
    private int last_processed_download_set_id;

    private Map short_id_map = new HashMap();
    private Map full_id_map = new HashMap();

    private Set actively_tracking = new HashSet();

    private CopyOnWriteSet<Peer> buddy_peers = new CopyOnWriteSet<Peer>(true);

    private CopyOnWriteList listeners = new CopyOnWriteList();

    private TimerEventPeriodic buddy_stats_timer;

    private Average buddy_receive_speed = Average.getInstance(1000, 10);

    private Average buddy_send_speed = Average.getInstance(1000, 10);

    public BuddyPluginTracker(BuddyPlugin _plugin, final BooleanParameter tracker_enable) {
        plugin = _plugin;

        tracker_enabled = tracker_enable.getValue();

        tracker_enable.addListener(new ParameterListener() {
            public void parameterChanged(Parameter param) {
                tracker_enabled = tracker_enable.getValue();

                checkEnabledState();
            }
        });

        // Assumed if we already have a plugin reference, that the
        // Azureus Core is available
        GlobalManager gm = AzureusCoreFactory.getSingleton().getGlobalManager();

        gm.addListener(new GlobalManagerAdapter() {
            public void seedingStatusChanged(boolean seeding_only_mode, boolean potentially_seeding_only) {
                seeding_only = potentially_seeding_only;

                checkEnabledState();
            }
        }, false);

        seeding_only = gm.isPotentiallySeedingOnly();

        checkEnabledState();
    }

    public void initialise() {
        plugin_enabled = plugin.isClassicEnabled();

        checkEnabledState();

        List buddies = plugin.getBuddies();

        for (int i = 0; i < buddies.size(); i++) {

            buddyAdded((BuddyPluginBuddy) buddies.get(i));
        }

        plugin.addListener(this);

        plugin.getAZ2Handler().addTrackerListener(this);

        plugin.getPluginInterface().getDownloadManager().addListener(this, true);
    }

    public void tick(int tick_count) {
        if (tick_count % TRACK_CHECK_TICKS == 0) {

            checkTracking();
        }

        if ((tick_count - 1) % TRACK_CHECK_TICKS == 0) {

            doTracking();
        }

        if (tick_count % PEER_CHECK_TICKS == 0) {

            checkPeers();
        }

        if (tick_count % PEER_RECHECK_TICKS == 0) {

            recheckPeers();
        }
    }

    public int getNetworkStatus() {
        return (network_status);
    }

    public long getNetworkReceiveBytesPerSecond() {
        return (buddy_receive_speed.getAverage());
    }

    public long getNetworkSendBytesPerSecond() {
        return (buddy_send_speed.getAverage());
    }

    protected void doTracking() {
        if (!(plugin_enabled && tracker_enabled)) {

            return;
        }

        Map to_do = new HashMap();

        Set active_set = new HashSet();

        synchronized (online_buddies) {

            Iterator it = online_buddies.iterator();

            while (it.hasNext()) {

                BuddyPluginBuddy buddy = (BuddyPluginBuddy) it.next();

                buddyData buddy_data = getBuddyData(buddy);

                Map active = buddy_data.getDownloadsToTrack();

                if (active.size() > 0) {

                    Iterator it2 = active.entrySet().iterator();

                    List track_now = new ArrayList();

                    while (it2.hasNext()) {

                        Map.Entry entry = (Map.Entry) it2.next();

                        Download dl = (Download) entry.getKey();
                        boolean now = ((Boolean) entry.getValue()).booleanValue();

                        if (now) {

                            track_now.add(dl);
                        }

                        active_set.add(dl);
                    }

                    if (track_now.size() > 0) {

                        to_do.put(buddy, track_now);
                    }
                }
            }
        }

        synchronized (actively_tracking) {

            Iterator it = active_set.iterator();

            while (it.hasNext()) {

                Download dl = (Download) it.next();

                if (!actively_tracking.contains(dl)) {

                    actively_tracking.add(dl);

                    trackPeers(dl);
                }
            }

            it = actively_tracking.iterator();

            while (it.hasNext()) {

                Download dl = (Download) it.next();

                if (!active_set.contains(dl)) {

                    it.remove();

                    untrackPeers(dl);
                }
            }
        }

        Iterator it = to_do.entrySet().iterator();

        while (it.hasNext()) {

            Map.Entry entry = (Map.Entry) it.next();

            BuddyPluginBuddy buddy = (BuddyPluginBuddy) entry.getKey();

            if (!buddy.isOnline(false)) {

                continue;
            }

            InetAddress ip = buddy.getAdjustedIP();

            if (ip == null) {

                continue;
            }

            int tcp_port = buddy.getTCPPort();
            int udp_port = buddy.getUDPPort();

            List downloads = (List) entry.getValue();

            for (int i = 0; i < downloads.size(); i++) {

                Download download = (Download) downloads.get(i);

                PeerManager pm = download.getPeerManager();

                if (pm == null) {

                    continue;
                }

                Peer[] existing_peers = pm.getPeers(ip.getHostAddress());

                boolean connected = false;

                for (int j = 0; j < existing_peers.length; j++) {

                    Peer peer = existing_peers[j];

                    if (peer.getTCPListenPort() == tcp_port || peer.getUDPListenPort() == udp_port) {

                        connected = true;

                        break;
                    }
                }

                if (connected) {

                    log(download.getName() + " - peer " + ip.getHostAddress() + " already connected");

                    continue;
                }

                log(download.getName() + " - connecting to peer " + ip.getHostAddress());

                PEPeerManager c_pm = PluginCoreUtils.unwrap(pm);

                Map user_data = new LightHashMap();

                user_data.put(PEER_KEY, download);

                user_data.put(Peer.PR_PRIORITY_CONNECTION, new Boolean(true));

                c_pm.addPeer(ip.getHostAddress(), tcp_port, udp_port, true, user_data);
            }
        }
    }

    protected void checkTracking() {
        if (!(plugin_enabled && tracker_enabled)) {

            return;
        }

        List online;

        synchronized (online_buddies) {

            online = new ArrayList(online_buddies);
        }

        Set downloads;
        int downloads_id;

        synchronized (tracked_downloads) {

            boolean downloads_changed = last_processed_download_set_id != download_set_id;

            if (downloads_changed) {

                last_processed_download_set = new HashSet(tracked_downloads);
                last_processed_download_set_id = download_set_id;
            }

            downloads = last_processed_download_set;
            downloads_id = last_processed_download_set_id;
        }

        Map diff_map = new HashMap();

        for (int i = 0; i < online.size(); i++) {

            BuddyPluginBuddy buddy = (BuddyPluginBuddy) online.get(i);

            buddyData buddy_data = getBuddyData(buddy);

            buddy_data.updateLocal(downloads, downloads_id, diff_map);
        }
    }

    public void initialised(boolean available) {
    }

    public void buddyAdded(BuddyPluginBuddy buddy) {
        buddyChanged(buddy);
    }

    public void buddyRemoved(BuddyPluginBuddy buddy) {
        buddyChanged(buddy);
    }

    public void buddyChanged(BuddyPluginBuddy buddy) {
        if (buddy.isOnline(false)) {

            addBuddy(buddy);

        } else {

            removeBuddy(buddy);
        }
    }

    protected buddyData getBuddyData(BuddyPluginBuddy buddy) {
        synchronized (online_buddies) {

            buddyData buddy_data = (buddyData) buddy.getUserData(BuddyPluginTracker.class);

            if (buddy_data == null) {

                buddy_data = new buddyData(buddy);

                buddy.setUserData(BuddyPluginTracker.class, buddy_data);
            }

            return (buddy_data);
        }
    }

    protected buddyData addBuddy(BuddyPluginBuddy buddy) {
        synchronized (online_buddies) {

            if (!online_buddies.contains(buddy)) {

                online_buddies.add(buddy);
            }

            buddyData bd = getBuddyData(buddy);

            if (bd.hasIPChanged()) {

                String ip = bd.getIP();

                if (ip != null) {

                    List l = (List) online_buddy_ips.get(ip);

                    if (l != null) {

                        l.remove(buddy);

                        if (l.size() == 0) {

                            online_buddy_ips.remove(ip);
                        }
                    }
                }

                bd.updateIP();

                ip = bd.getIP();

                if (ip != null) {

                    List l = (List) online_buddy_ips.get(ip);

                    if (l == null) {

                        l = new ArrayList();

                        online_buddy_ips.put(ip, l);
                    }

                    l.add(buddy);
                }
            }

            return (bd);
        }
    }

    protected void removeBuddy(BuddyPluginBuddy buddy) {
        synchronized (online_buddies) {

            if (online_buddies.contains(buddy)) {

                buddyData bd = getBuddyData(buddy);

                online_buddies.remove(buddy);

                String ip = bd.getIP();

                if (ip != null) {

                    List l = (List) online_buddy_ips.get(ip);

                    if (l != null) {

                        l.remove(buddy);

                        if (l.size() == 0) {

                            online_buddy_ips.remove(ip);
                        }
                    }
                }
            }
        }
    }

    protected int isBuddy(Peer peer) {
        String peer_ip = peer.getIp();

        List ips = AddressUtils.getLANAddresses(peer_ip);

        synchronized (online_buddies) {

            int result = BUDDY_NO;

            String tested = "";
            outer: for (int i = 0; i < ips.size(); i++) {

                String ip = (String) ips.get(i);

                tested += ip;

                List buddies = (List) online_buddy_ips.get(ip);

                if (buddies != null) {

                    if (peer.getTCPListenPort() == 0 && peer.getUDPListenPort() == 0) {

                        result = BUDDY_MAYBE;

                    } else {

                        for (int j = 0; j < buddies.size(); j++) {

                            BuddyPluginBuddy buddy = (BuddyPluginBuddy) buddies.get(j);

                            if (buddy.getTCPPort() == peer.getTCPListenPort() && buddy.getTCPPort() != 0) {

                                result = BUDDY_YES;

                                break outer;
                            }

                            if (buddy.getUDPPort() == peer.getUDPListenPort() && buddy.getUDPPort() != 0) {

                                result = BUDDY_YES;

                                break outer;
                            }
                        }
                    }
                }
            }

            // log( "isBuddy: " + peer_ip + " -> " + result + ",tested=" + tested );

            return (result);
        }
    }

    public void messageLogged(String str, boolean error) {
    }

    public void enabledStateChanged(boolean _enabled) {
        plugin_enabled = _enabled;

        checkEnabledState();
    }

    public void updated() {
    }

    public boolean isEnabled() {
        synchronized (this) {

            return (plugin_enabled && tracker_enabled);
        }
    }

    protected void checkEnabledState() {
        boolean seeding_change = false;
        boolean enabled_change = false;

        synchronized (this) {

            boolean old_enabled = old_plugin_enabled && old_tracker_enabled;

            if (plugin_enabled != old_plugin_enabled) {

                log("Plugin enabled state changed to " + plugin_enabled);

                old_plugin_enabled = plugin_enabled;
            }

            if (tracker_enabled != old_tracker_enabled) {

                log("Tracker enabled state changed to " + tracker_enabled);

                old_tracker_enabled = tracker_enabled;
            }

            if (seeding_only != old_seeding_only) {

                log("Seeding-only state changed to " + seeding_only);

                old_seeding_only = seeding_only;

                seeding_change = true;
            }

            enabled_change = old_enabled != (plugin_enabled && tracker_enabled);
        }

        if (seeding_change) {

            updateSeedingMode();
        }

        if (enabled_change) {

            fireEnabledChanged(isEnabled());
        }
    }

    protected void updateSeedingMode() {
        updateNetworkStatus();

        List online;

        synchronized (online_buddies) {

            online = new ArrayList(online_buddies);
        }

        for (int i = 0; i < online.size(); i++) {

            buddyData buddy_data = getBuddyData((BuddyPluginBuddy) online.get(i));

            if (buddy_data.hasDownloadsInCommon()) {

                buddy_data.updateStatus();
            }
        }
    }

    public void downloadAdded(final Download download) {
        Torrent t = download.getTorrent();

        if (t == null) {

            return;
        }

        if (t.isPrivate()) {

            download.addTrackerListener(new DownloadTrackerListener() {
                public void scrapeResult(DownloadScrapeResult result) {
                }

                public void announceResult(DownloadAnnounceResult result) {
                    if (okToTrack(download)) {

                        trackDownload(download);

                    } else {

                        untrackDownload(download);
                    }
                }
            }, false);
        }

        if (okToTrack(download)) {

            trackDownload(download);
        }

        download.addListener(new DownloadListener() {
            public void stateChanged(Download download, int old_state, int new_state) {
                if (okToTrack(download)) {

                    trackDownload(download);

                } else {

                    untrackDownload(download);
                }
            }

            public void positionChanged(Download download, int oldPosition, int newPosition) {
            }
        });
    }

    public void downloadRemoved(Download download) {
        untrackDownload(download);
    }

    protected void trackDownload(Download download) {
        synchronized (tracked_downloads) {

            if (tracked_downloads.contains(download)) {

                return;
            }

            downloadData download_data = new downloadData(download);

            download.setUserData(BuddyPluginTracker.class, download_data);

            HashWrapper full_id = download_data.getID();

            HashWrapper short_id = new HashWrapper(full_id.getHash(), 0, 4);

            full_id_map.put(full_id, download);

            List dls = (List) short_id_map.get(short_id);

            if (dls == null) {

                dls = new ArrayList();

                short_id_map.put(short_id, dls);
            }

            dls.add(download);

            tracked_downloads.add(download);

            download_set_id++;
        }
    }

    protected void untrackDownload(Download download) {
        synchronized (tracked_downloads) {

            if (tracked_downloads.remove(download)) {

                download_set_id++;

                downloadData download_data = (downloadData) download.getUserData(BuddyPluginTracker.class);

                download.setUserData(BuddyPluginTracker.class, null);

                HashWrapper full_id = download_data.getID();

                full_id_map.remove(full_id);

                HashWrapper short_id = new HashWrapper(full_id.getHash(), 0, SHORT_ID_SIZE);

                List dls = (List) short_id_map.get(short_id);

                if (dls != null) {

                    dls.remove(download);

                    if (dls.size() == 0) {

                        short_id_map.remove(short_id);
                    }
                }
            }
        }

        synchronized (online_buddies) {

            Iterator it = online_buddies.iterator();

            while (it.hasNext()) {

                BuddyPluginBuddy buddy = (BuddyPluginBuddy) it.next();

                buddyData buddy_data = getBuddyData(buddy);

                buddy_data.resetTracking(download);
            }
        }

        synchronized (actively_tracking) {

            actively_tracking.remove(download);
        }
    }

    protected void trackPeers(final Download download) {
        PeerManager pm = download.getPeerManager();

        // not running

        if (pm == null) {

            synchronized (actively_tracking) {

                actively_tracking.remove(download);
            }
        } else {

            log("Tracking peers for " + download.getName());

            download.addPeerListener(this);
        }
    }

    public void peerManagerAdded(Download download, PeerManager peer_manager) {
        trackPeers(download, peer_manager);
    }

    public void peerManagerRemoved(Download download, PeerManager peer_manager) {
        synchronized (actively_tracking) {

            actively_tracking.remove(download);
        }

        download.removePeerListener(this);
    }

    protected void trackPeers(final Download download, PeerManager pm) {
        pm.addListener(new PeerManagerListener() {
            public void peerAdded(PeerManager manager, Peer peer) {
                synchronized (actively_tracking) {

                    if (!actively_tracking.contains(download)) {

                        manager.removeListener(this);

                        return;
                    }
                }

                trackPeer(download, peer);
            }

            public void peerRemoved(PeerManager manager, Peer peer) {
            }
        });

        Peer[] peers = pm.getPeers();

        for (int i = 0; i < peers.length; i++) {

            trackPeer(download, peers[i]);
        }
    }

    protected void trackPeer(final Download download, final Peer peer) {
        int type = isBuddy(peer);

        if (type == BUDDY_YES) {

            markBuddyPeer(download, peer);

        } else if (type == BUDDY_MAYBE) {

            // mark as peer early so that we get optimistic disconnect if needed

            markBuddyPeer(download, peer);

            PeerListener2 listener = new PeerListener2() {
                public void eventOccurred(PeerEvent event) {
                    if (event.getType() == PeerEvent.ET_STATE_CHANGED) {

                        if (((Integer) event.getData()).intValue() == Peer.TRANSFERING) {

                            peer.removeListener(this);

                            // withdraw buddy marker if it turns out our earlier optimism
                            // was misplaced

                            if (isBuddy(peer) != BUDDY_YES) {

                                unmarkBuddyPeer(peer);
                            }
                        }
                    }
                }
            };

            peer.addListener(listener);

            if (peer.getState() == Peer.TRANSFERING) {

                peer.removeListener(listener);

                // withdraw buddy marker if it turns out our earlier optimism
                // was misplaced

                if (isBuddy(peer) != BUDDY_YES) {

                    unmarkBuddyPeer(peer);
                }
            }
        }
    }

    protected void untrackPeers(Download download) {
        log("Not tracking peers for " + download.getName());

        download.removePeerListener(this);

        PeerManager pm = download.getPeerManager();

        if (pm != null) {

            Peer[] peers = pm.getPeers();

            for (int i = 0; i < peers.length; i++) {

                Peer peer = peers[i];

                unmarkBuddyPeer(peer);
            }
        }
    }

    protected void markBuddyPeer(final Download download, final Peer peer) {
        boolean state_changed = false;

        synchronized (buddy_peers) {

            if (!buddy_peers.contains(peer)) {

                log("Adding buddy peer " + peer.getIp());

                if (buddy_peers.size() == 0) {

                    if (buddy_stats_timer == null) {

                        buddy_stats_timer = SimpleTimer.addPeriodicEvent("BuddyTracker:stats", 1000, new TimerEventPerformer() {
                            public void perform(TimerEvent event) {
                                Iterator it = buddy_peers.iterator();

                                long total_sent = 0;
                                long total_received = 0;

                                while (it.hasNext()) {

                                    Peer p = (Peer) it.next();

                                    PeerStats ps = p.getStats();

                                    long sent = ps.getTotalSent();
                                    long received = ps.getTotalReceived();

                                    long[] last = (long[]) p.getUserData(PEER_STATS_KEY);

                                    if (last != null) {

                                        total_sent += sent - last[0];
                                        total_received += received - last[1];
                                    }

                                    p.setUserData(PEER_STATS_KEY, new long[] { sent, received });
                                }

                                buddy_receive_speed.addValue(total_received);
                                buddy_send_speed.addValue(total_sent);
                            }
                        });
                    }

                    state_changed = true;
                }

                buddy_peers.add(peer);

                peer.setUserData(PEER_KEY, download);

                peer.setPriorityConnection(true);

                log(download.getName() + ": adding buddy peer " + peer.getIp());

                peer.addListener(new PeerListener2() {
                    public void eventOccurred(PeerEvent event) {
                        if (event.getType() == PeerEvent.ET_STATE_CHANGED) {

                            int state = ((Integer) event.getData()).intValue();

                            if (state == Peer.CLOSING || state == Peer.DISCONNECTED) {

                                peer.removeListener(this);

                                unmarkBuddyPeer(peer);
                            }
                        }
                    }
                });
            }
        }

        if (peer.getState() == Peer.CLOSING || peer.getState() == Peer.DISCONNECTED) {

            unmarkBuddyPeer(peer);
        }

        if (state_changed) {

            updateNetworkStatus();
        }
    }

    protected void unmarkBuddyPeer(Peer peer) {
        boolean state_changed = false;

        synchronized (buddy_peers) {

            Download download = (Download) peer.getUserData(PEER_KEY);

            if (download == null) {

                return;
            }

            if (buddy_peers.remove(peer)) {

                if (buddy_peers.size() == 0) {

                    state_changed = true;

                    if (buddy_stats_timer != null) {

                        buddy_stats_timer.cancel();

                        buddy_stats_timer = null;
                    }
                }

                log(download.getName() + ": removing buddy peer " + peer.getIp());
            }

            peer.setUserData(PEER_KEY, null);

            peer.setPriorityConnection(false);
        }

        if (state_changed) {

            updateNetworkStatus();
        }
    }

    protected void checkPeers() {
        List to_unmark = new ArrayList();

        synchronized (buddy_peers) {

            Iterator it = buddy_peers.iterator();

            while (it.hasNext()) {

                Peer peer = (Peer) it.next();

                if (peer.getState() == Peer.CLOSING || peer.getState() == Peer.DISCONNECTED) {

                    to_unmark.add(peer);
                }
            }
        }

        for (int i = 0; i < to_unmark.size(); i++) {

            unmarkBuddyPeer((Peer) to_unmark.get(i));
        }
    }

    protected void recheckPeers() {
        // go over peers for active torrents to see if we've missed and. can really only
        // happen with multi-homed LAN setups where a new (and utilised) route is found
        // after we start tracking

        synchronized (actively_tracking) {

            Iterator it = actively_tracking.iterator();

            while (it.hasNext()) {

                Download download = (Download) it.next();

                PeerManager pm = download.getPeerManager();

                if (pm != null) {

                    Peer[] peers = pm.getPeers();

                    for (int i = 0; i < peers.length; i++) {

                        trackPeer(download, peers[i]);
                    }
                }
            }
        }
    }

    protected void updateNetworkStatus() {
        int new_status;
        boolean changed = false;

        synchronized (buddy_peers) {

            if (buddy_peers.size() == 0) {

                new_status = BUDDY_NETWORK_IDLE;

            } else {

                new_status = seeding_only ? BUDDY_NETWORK_OUTBOUND : BUDDY_NETWORK_INBOUND;
            }

            if (new_status != network_status) {

                network_status = new_status;

                changed = true;
            }
        }

        if (changed) {

            fireStateChange(new_status);
        }
    }

    public void addListener(BuddyPluginTrackerListener l) {
        listeners.add(l);
    }

    public void removeListener(BuddyPluginTrackerListener l) {
        listeners.remove(l);
    }

    protected void fireStateChange(int state) {
        Iterator it = listeners.iterator();

        while (it.hasNext()) {

            try {
                ((BuddyPluginTrackerListener) it.next()).networkStatusChanged(this, state);

            } catch (Throwable e) {

                Debug.out(e);
            }
        }
    }

    protected void fireEnabledChanged(boolean enabled) {
        Iterator it = listeners.iterator();

        while (it.hasNext()) {

            try {
                ((BuddyPluginTrackerListener) it.next()).enabledStateChanged(this, enabled);

            } catch (Throwable e) {

                Debug.out(e);
            }
        }
    }

    protected void sendMessage(BuddyPluginBuddy buddy, int type, Map body) {
        Map msg = new HashMap();

        msg.put("type", new Long(type));
        msg.put("msg", body);

        plugin.getAZ2Handler().sendAZ2TrackerMessage(buddy, msg, BuddyPluginTracker.this);
    }

    public Map messageReceived(BuddyPluginBuddy buddy, Map message) {
        buddyData buddy_data = buddyAlive(buddy);

        int type = ((Long) message.get("type")).intValue();

        Map msg = (Map) message.get("msg");

        return (buddy_data.receiveMessage(type, msg));
    }

    public void messageFailed(BuddyPluginBuddy buddy, Throwable cause) {
        log("Failed to send message to " + buddy.getName(), cause);

        buddyDead(buddy);
    }

    protected buddyData buddyAlive(BuddyPluginBuddy buddy) {
        buddyData buddy_data = addBuddy(buddy);

        buddy_data.setAlive(true);

        return (buddy_data);
    }

    protected void buddyDead(BuddyPluginBuddy buddy) {
        buddyData buddy_data = getBuddyData(buddy);

        if (buddy_data != null) {

            buddy_data.setAlive(false);
        }
    }

    protected boolean okToTrack(Download d) {
        Torrent t = d.getTorrent();

        if (t == null) {

            return (false);
        }

        // only track private torrents if we have successfully received peers from tracker
        // which means we have the torrent legitimately. As this rule is enforced by both
        // ends of the tracking operation it means we will only track between peers that
        // both have a legitimate copy of the torrent.

        if (t.isPrivate()) {

            DownloadAnnounceResult announce = d.getLastAnnounceResult();

            if (announce == null || announce.getResponseType() != DownloadAnnounceResult.RT_SUCCESS || announce.getPeers().length < 2) {

                return (false);
            }
        }

        int state = d.getState();

        return (state != Download.ST_ERROR && state != Download.ST_STOPPING && state != Download.ST_STOPPED);
    }

    protected void log(String str) {
        plugin.log("Tracker: " + str);
    }

    protected void log(String str, boolean verbose) {
        if (verbose) {

            if (Constants.isCVSVersion()) {

                log(str);
            }
        } else {

            log(str);
        }
    }

    protected void log(String str, Throwable e) {
        plugin.log("Tracker: " + str, e);
    }

    private class buddyData {
        private BuddyPluginBuddy buddy;

        private Set downloads_sent;
        private int downloads_sent_id;

        private Map downloads_in_common;
        private boolean buddy_seeding_only;

        private int consecutive_fails;
        private long last_fail;

        private String current_ip;

        protected buddyData(BuddyPluginBuddy _buddy) {
            buddy = _buddy;
        }

        protected void updateIP() {
            InetAddress latest_ip = buddy.getAdjustedIP();

            if (latest_ip != null) {

                current_ip = latest_ip.getHostAddress();

                log("IP set to " + current_ip);
            }
        }

        protected boolean hasIPChanged() {
            InetAddress latest_ip = buddy.getAdjustedIP();

            if (latest_ip == null && current_ip == null) {

                return (false);

            } else if (latest_ip == null || current_ip == null) {

                return (true);

            } else {

                return (!current_ip.equals(latest_ip.getHostAddress()));
            }
        }

        protected String getIP() {
            return (current_ip);
        }

        protected boolean hasDownloadsInCommon() {
            synchronized (this) {

                return (downloads_in_common != null);
            }
        }

        protected void setAlive(boolean alive) {
            synchronized (this) {

                if (alive) {

                    consecutive_fails = 0;
                    last_fail = 0;

                } else {

                    consecutive_fails++;

                    last_fail = SystemTime.getMonotonousTime();
                }
            }
        }

        protected void updateLocal(Set downloads, int id, Map diff_map) {
            if (consecutive_fails > 0) {

                long retry_millis = RETRY_SEND_MIN;

                for (int i = 0; i < consecutive_fails - 1; i++) {

                    retry_millis <<= 2;

                    if (retry_millis > RETRY_SEND_MAX) {

                        retry_millis = RETRY_SEND_MAX;

                        break;
                    }
                }

                long now = SystemTime.getMonotonousTime();

                if (now - last_fail >= retry_millis) {

                    last_fail = now; // assume we're going to fail so we avoid
                                     // falling through here multiple times before
                                     // actuallt failing again

                    downloads_sent = null;
                    downloads_sent_id = 0;
                }
            }

            // first check to see if completion state changed for any common downloads

            List comp_changed = new ArrayList();

            synchronized (this) {

                if (downloads_in_common != null) {

                    Iterator it = downloads_in_common.entrySet().iterator();

                    while (it.hasNext()) {

                        Map.Entry entry = (Map.Entry) it.next();

                        Download d = (Download) entry.getKey();

                        buddyDownloadData bdd = (buddyDownloadData) entry.getValue();

                        boolean local_complete = d.isComplete(false);

                        if (local_complete != bdd.isLocalComplete()) {

                            bdd.setLocalComplete(local_complete);

                            comp_changed.add(d);
                        }
                    }
                }
            }

            if (comp_changed.size() > 0) {

                byte[][] change_details = exportFullIDs(comp_changed);

                if (change_details[0].length > 0) {

                    Map msg = new HashMap();

                    msg.put("seeding", new Long(seeding_only ? 1 : 0));

                    msg.put("change", change_details[0]);
                    msg.put("change_s", change_details[1]);

                    sendMessage(buddy, REQUEST_TRACKER_CHANGE, msg);
                }
            }

            if (id == downloads_sent_id) {

                return;
            }

            Long key = new Long(((long) id) << 32 | (long) downloads_sent_id);

            Object[] diffs = (Object[]) diff_map.get(key);

            boolean incremental = downloads_sent != null;

            byte[] added_bytes;
            byte[] removed_bytes;

            if (diffs == null) {

                List added;
                List removed = new ArrayList();

                if (downloads_sent == null) {

                    added = new ArrayList(downloads);

                } else {

                    added = new ArrayList();

                    Iterator it1 = downloads.iterator();

                    while (it1.hasNext()) {

                        Download download = (Download) it1.next();

                        if (okToTrack(download)) {

                            if (!downloads_sent.contains(download)) {

                                added.add(download);
                            }
                        }
                    }

                    Iterator it2 = downloads_sent.iterator();

                    while (it2.hasNext()) {

                        Download download = (Download) it2.next();

                        if (!downloads.contains(download)) {

                            removed.add(download);
                        }
                    }
                }

                added_bytes = exportShortIDs(added);
                removed_bytes = exportFullIDs(removed)[0];

                diff_map.put(key, new Object[] { added_bytes, removed_bytes });
            } else {

                added_bytes = (byte[]) diffs[0];
                removed_bytes = (byte[]) diffs[1];
            }

            downloads_sent = downloads;
            downloads_sent_id = id;

            if (added_bytes.length == 0 && removed_bytes.length == 0) {

                return;
            }

            Map msg = new HashMap();

            if (added_bytes.length > 0) {

                msg.put("added", added_bytes);
            }

            if (removed_bytes.length > 0) {

                msg.put("removed", removed_bytes);
            }

            msg.put("inc", new Long(incremental ? 1 : 0));
            msg.put("seeding", new Long(seeding_only ? 1 : 0));

            sendMessage(buddy, REQUEST_TRACKER_SUMMARY, msg);
        }

        protected Map updateRemote(Map msg) {
            List added = importShortIDs((byte[]) msg.get("added"));

            Map reply = new HashMap();

            byte[][] add_details = exportFullIDs(added);

            if (add_details[0].length > 0) {

                reply.put("added", add_details[0]);
                reply.put("added_s", add_details[1]);
            }

            synchronized (this) {

                if (downloads_in_common != null) {

                    Map removed = importFullIDs((byte[]) msg.get("removed"), null);

                    Iterator it = removed.keySet().iterator();

                    while (it.hasNext()) {

                        Download d = (Download) it.next();

                        if (downloads_in_common.remove(d) != null) {

                            log("Removed " + d.getName() + " common download", false, true);
                        }
                    }

                    if (downloads_in_common.size() == 0) {

                        downloads_in_common = null;
                    }
                }
            }

            return (reply);
        }

        protected void updateCommonDownloads(Map downloads, boolean incremental) {
            synchronized (this) {

                if (downloads_in_common == null) {

                    downloads_in_common = new HashMap();

                } else {

                    // if not incremental then remove any downloads that no longer
                    // are in common

                    if (!incremental) {

                        Iterator it = downloads_in_common.keySet().iterator();

                        while (it.hasNext()) {

                            Download download = (Download) it.next();

                            if (!downloads.containsKey(download)) {

                                log("Removing " + download.getName() + " from common downloads", false, true);

                                it.remove();
                            }
                        }
                    }
                }

                Iterator it = downloads.entrySet().iterator();

                while (it.hasNext()) {

                    Map.Entry entry = (Map.Entry) it.next();

                    Download d = (Download) entry.getKey();

                    buddyDownloadData bdd = (buddyDownloadData) entry.getValue();

                    buddyDownloadData existing = (buddyDownloadData) downloads_in_common.get(d);

                    if (existing == null) {

                        log("Adding " + d.getName() + " to common downloads (bdd=" + bdd.getString() + ")", false, true);

                        downloads_in_common.put(d, bdd);

                    } else {

                        boolean old_rc = existing.isRemoteComplete();
                        boolean new_rc = bdd.isRemoteComplete();

                        if (old_rc != new_rc) {

                            existing.setRemoteComplete(new_rc);

                            log("Changing " + d.getName() + " common downloads (bdd=" + existing.getString() + ")", false, true);
                        }
                    }
                }

                if (downloads_in_common.size() == 0) {

                    downloads_in_common = null;
                }
            }
        }

        protected void updateStatus() {
            Map msg = new HashMap();

            msg.put("seeding", new Long(seeding_only ? 1 : 0));

            sendMessage(buddy, REQUEST_TRACKER_STATUS, msg);
        }

        protected Map receiveMessage(int type, Map msg_in) {
            int reply_type = -1;
            Map msg_out = null;

            Long l_seeding = (Long) msg_in.get("seeding");

            if (l_seeding != null) {

                boolean old = buddy_seeding_only;

                buddy_seeding_only = l_seeding.intValue() == 1;

                if (old != buddy_seeding_only) {

                    log("Seeding only changed to " + buddy_seeding_only);
                }
            }

            if (type == REQUEST_TRACKER_SUMMARY) {

                reply_type = REPLY_TRACKER_SUMMARY;

                msg_out = updateRemote(msg_in);

                msg_out.put("inc", msg_in.get("inc"));

            } else if (type == REQUEST_TRACKER_STATUS) {

                reply_type = REPLY_TRACKER_STATUS;

            } else if (type == REQUEST_TRACKER_CHANGE) {

                reply_type = REPLY_TRACKER_STATUS;

                Map downloads = importFullIDs((byte[]) msg_in.get("changed"), (byte[]) msg_in.get("changed_s"));

                updateCommonDownloads(downloads, true);

            } else if (type == REQUEST_TRACKER_ADD) {

                reply_type = REPLY_TRACKER_ADD;

                Map downloads = importFullIDs((byte[]) msg_in.get("added"), (byte[]) msg_in.get("added_s"));

                updateCommonDownloads(downloads, true);

            } else if (type == REPLY_TRACKER_SUMMARY) {

                // full hashes on reply

                byte[] possible_matches = (byte[]) msg_in.get("added");
                byte[] possible_match_states = (byte[]) msg_in.get("added_s");

                boolean incremental = ((Long) msg_in.get("inc")).intValue() == 1;

                if (possible_matches != null && possible_match_states != null) {

                    Map downloads = importFullIDs(possible_matches, possible_match_states);

                    if (downloads.size() > 0) {

                        updateCommonDownloads(downloads, incremental);

                        byte[][] common_details = exportFullIDs(new ArrayList(downloads.keySet()));

                        if (common_details[0].length > 0) {

                            Map msg = new HashMap();

                            msg.put("seeding", new Long(seeding_only ? 1 : 0));

                            msg.put("added", common_details[0]);
                            msg.put("added_s", common_details[1]);

                            sendMessage(buddy, REQUEST_TRACKER_ADD, msg);
                        }
                    }
                }

            } else if (type == REPLY_TRACKER_CHANGE || type == REPLY_TRACKER_STATUS || type == REPLY_TRACKER_ADD) {

                // nothing interesting in reply for these
            } else {

                log("Unrecognised type " + type);
            }

            if (reply_type != -1) {

                Map reply = new HashMap();

                reply.put("type", new Long(reply_type));

                if (msg_out == null) {

                    msg_out = new HashMap();
                }

                msg_out.put("seeding", new Long(seeding_only ? 1 : 0));

                reply.put("msg", msg_out);

                return (reply);
            }

            return (null);
        }

        protected byte[] exportShortIDs(List downloads) {
            byte[] res = new byte[SHORT_ID_SIZE * downloads.size()];

            for (int i = 0; i < downloads.size(); i++) {

                Download download = (Download) downloads.get(i);

                downloadData download_data = (downloadData) download.getUserData(BuddyPluginTracker.class);

                if (download_data != null) {

                    System.arraycopy(download_data.getID().getBytes(), 0, res, i * SHORT_ID_SIZE, SHORT_ID_SIZE);
                }
            }

            return (res);
        }

        protected List importShortIDs(byte[] ids) {
            List res = new ArrayList();

            if (ids != null) {

                synchronized (tracked_downloads) {

                    for (int i = 0; i < ids.length; i += SHORT_ID_SIZE) {

                        List dls = (List) short_id_map.get(new HashWrapper(ids, i, SHORT_ID_SIZE));

                        if (dls != null) {

                            res.addAll(dls);
                        }
                    }
                }
            }

            return (res);
        }

        protected byte[][] exportFullIDs(List downloads) {
            byte[] hashes = new byte[FULL_ID_SIZE * downloads.size()];
            byte[] states = new byte[downloads.size()];

            for (int i = 0; i < downloads.size(); i++) {

                Download download = (Download) downloads.get(i);

                downloadData download_data = (downloadData) download.getUserData(BuddyPluginTracker.class);

                if (download_data != null) {

                    System.arraycopy(download_data.getID().getBytes(), 0, hashes, i * FULL_ID_SIZE, FULL_ID_SIZE);

                    states[i] = download.isComplete(false) ? (byte) 0x01 : (byte) 0x00;
                }
            }

            return (new byte[][] { hashes, states });
        }

        protected Map importFullIDs(byte[] ids, byte[] states) {
            Map res = new HashMap();

            if (ids != null) {

                synchronized (tracked_downloads) {

                    for (int i = 0; i < ids.length; i += FULL_ID_SIZE) {

                        Download dl = (Download) full_id_map.get(new HashWrapper(ids, i, FULL_ID_SIZE));

                        if (dl != null) {

                            buddyDownloadData bdd = new buddyDownloadData(dl);

                            if (states != null) {

                                bdd.setRemoteComplete((states[i / FULL_ID_SIZE] & 0x01) != 0);
                            }

                            res.put(dl, bdd);
                        }
                    }
                }
            }

            return (res);
        }

        protected Map getDownloadsToTrack() {
            Map res = new HashMap();

            if (seeding_only == buddy_seeding_only) {

                log("Not tracking, buddy and me both " + (seeding_only ? "seeding" : "downloading"), true, false);

                return (res);
            }

            long now = SystemTime.getMonotonousTime();

            synchronized (this) {

                if (downloads_in_common == null) {

                    log("Not tracking, buddy has nothing in common", true, false);

                    return (res);
                }

                Iterator it = downloads_in_common.entrySet().iterator();

                while (it.hasNext()) {

                    Map.Entry entry = (Map.Entry) it.next();

                    Download d = (Download) entry.getKey();

                    buddyDownloadData bdd = (buddyDownloadData) entry.getValue();

                    if (d.isComplete(false) && bdd.isRemoteComplete()) {

                        // both complete, nothing to do!

                        log(d.getName() + " - not tracking, both complete", true, true);

                    } else {

                        long last_track = bdd.getTrackTime();

                        if (last_track == 0 || now - last_track >= TRACK_INTERVAL) {

                            log(d.getName() + " - tracking", false, true);

                            bdd.setTrackTime(now);

                            res.put(d, new Boolean(true));

                        } else {

                            res.put(d, new Boolean(false));
                        }
                    }
                }
            }

            return (res);
        }

        protected void resetTracking(Download download) {
            synchronized (this) {

                if (downloads_in_common == null) {

                    return;
                }

                buddyDownloadData bdd = (buddyDownloadData) downloads_in_common.get(download);

                if (bdd != null) {

                    bdd.resetTrackTime();
                }
            }
        }

        protected void log(String str) {
            BuddyPluginTracker.this.log(buddy.getName() + ": " + str);
        }

        protected void log(String str, boolean verbose, boolean no_buddy) {
            BuddyPluginTracker.this.log((no_buddy ? "" : (buddy.getName() + ": ")) + str, verbose);
        }
    }

    private static class buddyDownloadData {
        private boolean local_is_complete;
        private boolean remote_is_complete;
        private long last_track;

        protected buddyDownloadData(Download download) {
            local_is_complete = download.isComplete(false);
        }

        protected void setLocalComplete(boolean b) {
            local_is_complete = b;
        }

        protected boolean isLocalComplete() {
            return (local_is_complete);
        }

        protected void setRemoteComplete(boolean b) {
            remote_is_complete = b;
        }

        protected boolean isRemoteComplete() {
            return (remote_is_complete);
        }

        protected void setTrackTime(long time) {
            last_track = time;
        }

        protected long getTrackTime() {
            return (last_track);
        }

        protected void resetTrackTime() {
            last_track = 0;
        }

        protected String getString() {
            return ("lic=" + local_is_complete + ",ric=" + remote_is_complete + ",lt=" + last_track);
        }
    }

    private static class downloadData {
        private static final byte[] IV = { (byte) 0x7A, (byte) 0x7A, (byte) 0xAD, (byte) 0xAB, (byte) 0x8E, (byte) 0xBF, (byte) 0xCD, (byte) 0x39,
                (byte) 0x87, (byte) 0x0, (byte) 0xA4, (byte) 0xB8, (byte) 0xFE, (byte) 0x40, (byte) 0xA2, (byte) 0xE8 };

        private HashWrapper id;

        protected downloadData(Download download) {
            Torrent t = download.getTorrent();

            if (t != null) {

                byte[] hash = t.getHash();

                SHA1 sha1 = new SHA1();

                sha1.update(ByteBuffer.wrap(IV));
                sha1.update(ByteBuffer.wrap(hash));

                id = new HashWrapper(sha1.digest());
            }
        }

        protected HashWrapper getID() {
            return (id);
        }
    }
}
